using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.WebTesting;
using System.CodeDom;
using Microsoft.CSharp;
using System.IO;
using System.CodeDom.Compiler;

namespace UKVSTS.WebTestPlugins
{
    class ScriptedParamDetails
    {
        /// <summary>
        /// The body of the script
        /// </summary>
        private String m_Code;

        /// <summary>
        /// The name of the parameter that is using this script
        /// </summary>
        private String m_ParamName;

        /// <summary>
        /// The compiled up script
        /// </summary>
        private MethodInfo m_Method;

        /// <summary>
        /// Use a delegate to store the address of the compiled up script.
        /// This will be quicker than calling it through reflection each time.
        /// </summary>
        private delegate object ScriptsGetValueDelegate (WebTestRequest request, WebTest test , WebTestContext context);

        /// <summary>
        /// The address of our compiled script.
        /// </summary>
        private ScriptsGetValueDelegate m_GetValueFunc;

        /// <summary>
        /// The namespace we will put our compiled script into.
        /// </summary>
        private const String GEN_NAMESPACE = "UKVSTS.WebTestPlugIns.Temp";

        /// <summary>
        /// The name for the method containing our script.
        /// </summary>
        private const String GEN_METHODNAME = "GetValue";

        /// <summary>
        /// Do we create a debug version of the code - this is to support dev of this plugin
        /// rather than support for the end user.
        /// </summary>
        private bool debug = true;

        /// <summary>
        /// List of namespaces that we will give import for the script block.
        /// </summary>
        private static String[] GEN_NAMESPACE_IMPORTS = new String [] { 
            "System", 
            "Microsoft.VisualStudio.TestTools.WebTesting"
        };

        /// <summary>
        /// Createas and compiles a script block
        /// </summary>
        /// <param name="paramName">The name of the param that is using this script</param>
        /// <param name="code">The body of this script</param>
        public ScriptedParamDetails(String paramName, String code)
        {
            debug = System.Diagnostics.Debugger.IsAttached;
            m_Code = code;
            m_ParamName = paramName;

            try
            {
                m_Method = CreateCodeAndReturnMethod(code, debug);

                // Create a delegate for this newly created method - this will be faster to
                // call the function than MethodInfo.Invoke
                // ..
                m_GetValueFunc = (ScriptsGetValueDelegate)Delegate.CreateDelegate(typeof(ScriptsGetValueDelegate), m_Method);
            }
            catch (Exception ex)
            {
                ScriptedParamException spx = ex as ScriptedParamException;
                if (spx == null)
                {
                    spx = new ScriptedParamException(ex);
                }
                spx.ParamName = ParamName;
                throw;
            }
        }

        /// <summary>
        /// Uses the codedom surroung the script with a metod, class and namespace.
        /// </summary>
        /// <param name="code">The body of the script</param>
        /// <param name="debug">Flag to indicate if a debug version should be created</param>
        /// <returns>The MethodInfo object that reflects the compiled function</returns>
        static MethodInfo CreateCodeAndReturnMethod(String code, bool debug)
        {
            // Create the container where our code will be written.
            //
            CodeCompileUnit compileUnit = new CodeCompileUnit();

            // Create a namespace for our class and include and reference namespaces
            CodeNamespace ns = CreateNamespace(compileUnit);

            // Invent a unique name for this type.
            //
            String typeName = "ScriptClass" + Guid.NewGuid().ToString().Replace('-', '_');
            
            // Create the class definition
            //
            CodeTypeDeclaration td = CreateTypeDeclaration(ns, typeName);

            // Create the metod that contains our script.
            //
            td.Members.Add(CreateMethod(code));

            // Build it
            //
            Assembly assm = CompileCodeToAssembly(compileUnit, debug);

            // Find our method
            //
            Type t = assm.GetType(GEN_NAMESPACE + "." + typeName);
            return t.GetMethod(GEN_METHODNAME, System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
        }

        static CodeMemberMethod CreateMethod (String body)
        {
            // Defines a method that returns a Object.
            //
            CodeMemberMethod method = new CodeMemberMethod();
            method.Name = GEN_METHODNAME;
            method.Attributes = MemberAttributes.Static | MemberAttributes.Public;
            method.ReturnType = new CodeTypeReference("System.Object");

            // Pass in a couple of useful args.
            //
            method.Parameters.Add(new CodeParameterDeclarationExpression("WebTestRequest", "Request"));
            method.Parameters.Add(new CodeParameterDeclarationExpression("WebTest", "WebTest"));
            method.Parameters.Add(new CodeParameterDeclarationExpression("WebTestContext", "Context"));

            // If the script is the form
            //   <% = .....
            // then we want to subsitute a return statement for the =
            // If there is no equals, then we don't want to inser a return.

            bool needsReturn = false;
            int i = 0;
            int charsToTrim = 0;
            do
            {
                if (body[i] == '=')
                {
                    needsReturn = true;
                    charsToTrim = i + 1;
                    break;
                }

            } while (Char.IsWhiteSpace(body[i++]));

            // Do we need to trim any chars of the string (i.e. white space and a =).
            //
            if (charsToTrim > 0)
            {
                body = body.Substring(charsToTrim);
            }


            if (needsReturn == true)
            {

                method.Statements.Add(new CodeMethodReturnStatement(new CodeSnippetExpression(body)));
            }
            else
            {
                method.Statements.Add(new CodeSnippetExpression(body));
            }

            return method;
        }


        static CodeTypeDeclaration CreateTypeDeclaration(CodeNamespace ns, String typeName)
        {
            // Creates a new type declaration.
            CodeTypeDeclaration newType = new CodeTypeDeclaration(typeName);
            
            // Sets the member attributes for the type.
            newType.Attributes = MemberAttributes.Public | MemberAttributes.Static;

            ns.Types.Add(newType);

            return newType;
        }

        private static CodeNamespace CreateNamespace(CodeCompileUnit compileUnit)
        {
            // Declare a new namespace.
            //
            CodeNamespace ns = new CodeNamespace(GEN_NAMESPACE);

            // Add the new namespace to the compile unit.
            compileUnit.Namespaces.Add(ns);

            // Add the new namespace import for the System namespace.
            foreach (String import in GEN_NAMESPACE_IMPORTS)
            {
                ns.Imports.Add(new CodeNamespaceImport(import));
            }

            return ns;
        }

        public static Assembly CompileCodeToAssembly(CodeCompileUnit compileUnit, bool debug)
        {
            // Generate the code with the C# code provider.
            CSharpCodeProvider provider = new CSharpCodeProvider();

            TextWriter tw = null;
            try
            {
                if (debug)
                {
                    StreamWriter sw = new StreamWriter(GEN_NAMESPACE + "." + compileUnit.Namespaces[0].Types[0].Name + ".cs");
                    tw = new IndentedTextWriter(sw, "    ");
                }
                else
                {
                    // Use an in memory buffer to generate the code to.
                    //
                    tw = new StringWriter(new StringBuilder(1024));
                }

                // Generate source code using the code provider.
                //
                provider.GenerateCodeFromCompileUnit(compileUnit, tw, new CodeGeneratorOptions());
            }
            finally
            {
                if (tw != null)
                {
                    tw.Close();
                }
            }

            CompilerParameters compilerParams = new CompilerParameters();

            // Need to ensure we have a reference to Microsoft.VisualStudio.QualityTools.WebTestFramework.dll.
            // However this isn't in the GAC - it is in memory, so find the location from the loaded assembly
            //
            compilerParams.ReferencedAssemblies.Add(typeof(WebTest).Assembly.Location);
            compilerParams.GenerateInMemory = !debug;

            compilerParams.IncludeDebugInformation = debug;

            CompilerResults results = provider.CompileAssemblyFromDom(compilerParams, compileUnit);

            String errString = String.Empty;

            foreach (CompilerError err in results.Errors)
            {
                if (debug)
                {
                    System.Diagnostics.Debug.WriteLine(String.Format("{0}({1},{2}): {3} {4}: {5}",
                                                                        err.FileName,
                                                                        err.Line,
                                                                        err.Column,
                                                                        err.IsWarning ? "Warning" : "Error",
                                                                        err.ErrorNumber,
                                                                        err.ErrorText));
                }
                if (err.IsWarning == false)
                {
                    errString += err.ErrorText + "\n";
                }
            }

            if (errString != String.Empty)
            {
                throw new ScriptedParamException("An error has occured when compiling a script: " + errString
                                                 + ". Please ensure that the script is of the format <%= value %> or <% return value; %>");
            }

            return results.CompiledAssembly;
        }


        public String Code
        {
            get { return m_Code; }
        }

        public String ParamName
        {
            get { return m_ParamName; }
        }

        /// <summary>
        /// Executes the script and returns whatever the script evaluated.
        /// </summary>
        /// <param name="e"></param>
        /// <returns></returns>
        public String InvokeScript(PreRequestEventArgs e)
        {
            try
            {
                Object retval = m_GetValueFunc(e.Request, e.WebTest, e.WebTest.Context);
                return retval == null ? "(null)" : retval.ToString();
            }
            catch (Exception ex)
            {
                throw new ScriptedParamException("Script execution error \"" + ex.Message + "\"", ParamName, ex);
            }
        }
    }
}
